/*
 Copyright (c) 2011, POLIDEA
 All rights reserved.

 Redistribution and use in source and binary forms, with or without modification, 
 are permitted provided that the following conditions are met:

 Redistributions of source code must retain the above copyright notice, 
 this list of conditions and the following disclaimer.
 Redistributions in binary form must reproduce the above copyright notice, 
 this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
 
 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 
 THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
 IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
 INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, 
 OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
 WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 
 EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package pl.polidea.android.utils.images;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.HashMap;
import java.util.Map;
import java.util.Vector;

import pl.polidea.android.utils.crypto.Crypto;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapFactory.Options;
import android.text.TextUtils;
import android.text.format.DateUtils;

public final class ImageDownloader {

	private final static String TAG = ImageDownloader.class.getSimpleName();

	private final File cacheDirectory;
	
	private final Thread[] workerThread = new Thread[4];
	
	private final Vector<LoadBitmapDescriptor> tasksLine = new Vector<LoadBitmapDescriptor>(10);
	private final Map<String,Vector<LoadBitmapDescriptor>> taskGroups = new HashMap<String, Vector<LoadBitmapDescriptor>>();
	private volatile boolean shutdownWorkerThreads = false;
	
	private final Object tasksSyncObject = new Object();
	
	public ImageDownloader(File cacheDir) {
		cacheDirectory = cacheDir;
		for ( int i = 0 ; i < workerThread.length ; ++i ) {
		    workerThread[i] = new LoaderThread(i % 2 == 0);
		    workerThread[i].start();
		}
		shutdownWorkerThreads = false;
	}
	
	public void finish() {
	    shutdownWorkerThreads = true;
	}
	
	public interface ImageDownloaderListener {
		void onBitmapLoaded(String url, Bitmap b, Object tag);
	    void onBitmapLoadError(String url, Object tag);
	}
	
	public void loadImage(String url, Object tag, String groupName, ImageDownloaderListener listener, int maxWidth, int maxHeight) {
        if ( !TextUtils.isEmpty(url) ) {
            final LoadBitmapDescriptor loadRunnable = new LoadBitmapDescriptor(url, groupName, tag, listener, maxWidth, maxHeight);
            synchronized (tasksSyncObject) {
                tasksLine.add(loadRunnable);
                if ( !TextUtils.isEmpty(groupName) ) {
                    if  ( !taskGroups.containsKey(groupName)  ) taskGroups.put(groupName, new Vector<LoadBitmapDescriptor>(10));
                    taskGroups.get(groupName).add(loadRunnable);
                }
            }
        }
    }
	
	public void cancelLoad(String groupName) {
	    synchronized (tasksSyncObject) {
	        final Vector<LoadBitmapDescriptor> tasksInGroup = taskGroups.get(groupName);
	        if ( tasksInGroup != null ) {
	            tasksLine.removeAll(tasksInGroup);
	        }
	        taskGroups.remove(groupName);
        }
	}
	
	private static String hashImageUrl(String url) {
	    return Crypto.MD5(url);
	}
	
	private File getImageFile(String url) {
	    final File imageCacheFile = new File(cacheDirectory, hashImageUrl(url));
	    if ( imageCacheFile.exists() ) {
	        return imageCacheFile;
	    } else {
	        return null;
	    }
	}
	
	private class LoaderThread extends Thread {
	    
	    private boolean loadOnlyFromDisk = false;

	    public LoaderThread(boolean loadOnlyFromDisk) {
	        this.loadOnlyFromDisk = loadOnlyFromDisk;
	    }
	    
	    @Override
	    public void run() {
	        android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND);
            while ( !shutdownWorkerThreads ) {
                while ( true) {
                    LoadBitmapDescriptor task = null;
                    synchronized (tasksSyncObject) {
                        if ( !tasksLine.isEmpty() ) task = tasksLine.get(0);
                        if ( task != null  ) {                            
                            if  ( loadOnlyFromDisk ) {
                                final File imageFile = getImageFile(task.url);
                                if ( imageFile != null ) {
                                    task = tasksLine.remove(0);
                                } else {
                                   task = null;
                                }
                            } else {
                                task = tasksLine.remove(0);
                            }                                                        
                        }
                        if ( task != null && !TextUtils.isEmpty(task.groupName)) {
                            if ( taskGroups.containsKey(task.groupName)) {
                                taskGroups.get(task.groupName).remove(task);
                            }
                        }
                    }
                    if ( task == null ) break;
                    loadImage(task);
                }
                try {
                    Thread.sleep(40);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
	    }
	    
	    public void loadImage(LoadBitmapDescriptor desc) {
            if ( !cacheDirectory.exists() || !cacheDirectory.isDirectory() ) {
                cacheDirectory.mkdirs();
            }
            final String hashedUrl = hashImageUrl(desc.url);
            final File imageCacheFile = new File(cacheDirectory, hashedUrl);
            if ( imageCacheFile.exists() && fileCanBeDecodedToBitmap(imageCacheFile) ) {
                final Bitmap resultBitmap = decodeBitmap(imageCacheFile, desc.maxWidth, desc.maxHeight);
                if ( resultBitmap == null ) {                   
                    imageCacheFile.delete();
                    if ( desc.listener != null ) desc.listener.onBitmapLoadError(desc.url, desc.tag);
                } else {
                    if ( desc.listener != null ) desc.listener.onBitmapLoaded(desc.url, resultBitmap, desc.tag);
                }
            } else {
                try {
                    final Bitmap resultBitmap = downloadAndSaveImage(imageCacheFile, desc.url, desc.maxWidth, desc.maxHeight);
                    if ( resultBitmap == null ) {
                        imageCacheFile.delete();
                        if ( desc.listener != null ) desc.listener.onBitmapLoadError(desc.url, desc.tag);
                    } else {
                        if ( desc.listener != null ) desc.listener.onBitmapLoaded(desc.url, resultBitmap, desc.tag);
                    } 
                } catch ( IOException e ) {
                    if ( desc.listener != null ) desc.listener.onBitmapLoadError(desc.url, desc.tag);
                    e.printStackTrace();
                }
            }
        }
        
        private final Bitmap decodeBitmap(File file,int maxWidth, int maxHeight) {
            
            final BitmapFactory.Options decodeBitmapOpts = new Options();
            decodeBitmapOpts.inScaled = false;
                
            final BitmapFactory.Options decodeSizeOpts = new Options();
            decodeSizeOpts.inScaled = false;
            decodeSizeOpts.inJustDecodeBounds = true;
            
            BitmapFactory.decodeFile(file.getAbsolutePath(), decodeSizeOpts);
            
            if ( maxHeight > 0 && maxWidth > 0 ) {
                decodeBitmapOpts.inSampleSize = Math.min(decodeSizeOpts.outWidth/maxWidth, decodeSizeOpts.outHeight/maxHeight);
            }
           
            return BitmapFactory.decodeFile(file.getAbsolutePath(), decodeBitmapOpts);
        }
        
        private final boolean fileCanBeDecodedToBitmap(File file) {
            final BitmapFactory.Options opt = new Options();
            opt.inJustDecodeBounds = true;
            BitmapFactory.decodeFile(file.getAbsolutePath(), opt);
            return opt.outWidth != -1 && opt.outHeight != -1;
        }
        
        private Bitmap downloadAndSaveImage(File saveFile, String url, int maxWidth, int maxHeight) throws IOException {
            final URLConnection currentConnection = (new URL(url)).openConnection();
            if ( currentConnection != null && currentConnection instanceof HttpURLConnection) {
                final HttpURLConnection httpConn = (HttpURLConnection)currentConnection;                
                httpConn.setRequestMethod("GET");
                httpConn.setDoInput(true);
                httpConn.setConnectTimeout((int)(15 * DateUtils.SECOND_IN_MILLIS));
                httpConn.setReadTimeout((int)(15 * DateUtils.SECOND_IN_MILLIS));
                HttpURLConnection.setFollowRedirects(true);
                final int responseCode = httpConn.getResponseCode();
                
            }
            final InputStream in = currentConnection.getInputStream();
            final FileOutputStream out = new FileOutputStream(saveFile);
            int recv = -1;
            final byte[] buffer = new byte[1024];
            while ( ( recv = in.read(buffer)) != -1 ) {
                out.write(buffer, 0, recv);
            }
            in.close();
            out.getFD().sync();
            out.close();
            return decodeBitmap(saveFile, maxWidth, maxHeight);
        }
	}
	
	private static class LoadBitmapDescriptor {

		public final String url;
		public final Object tag;
		public final ImageDownloaderListener listener;
		public final int maxWidth;
		public final int maxHeight;
		public final String groupName;
		
		public LoadBitmapDescriptor(String imageUrl, String groupName, Object tag, ImageDownloaderListener l, int maxWidth, int maxHeight) {
		    this.url = imageUrl;
			this.listener = l;
			this.tag = tag;
			this.maxHeight = maxHeight;
			this.maxWidth = maxWidth;
			this.groupName = groupName;
		}		
	}
}
